<?php
/**
 * @author Matías Mirabelli <project@moyrax.com>
 */
class ClassFactory
{
	/**
	 * Extensión predeterminada de las clases.
	 *
	 * @var string
	 */
	public static $classExtension = ".php";

	/**
	 * Guarda los nombres de las clases cargadas.
	 *
	 * @var array
	 */
	public $classNames = array();

	/**
	 * Clase vinculada a esta ClassFactory.
	 *
	 * @var class
	 */
	public $parentClass = null;

	/**
	 * Guarda los directorios donde se buscarán las
	 * clases al intentar cargarlas por el nombre.
	 *
	 * @var array
	 */
	protected $includePath = array();


	/**
	 * Carga todos los archivos php del directorio especificado.
	 *
	 * @param $path Directorio en el que se buscarán los archivos.
	 */
	public function loadClassesFromDir( $path )
	{
		// Abre el directorio para listar archivos.
		//
		$hdir = @opendir($path);

		// Recorre los archivos.
		//
		while( $file = readdir($hdir) )
		{
		   $ext = substr($file, strlen($file) - 4, 4);

		   if ( $ext == self::$classExtension  )
		   {
				// Es un archivo PHP, lo carga.
				//
				require_once( $path . $file );

				// Guardamos el nombre
				//
				$base = substr( $file, 0, strlen($file) - 4 );

				$this->classNames[$base] = null;
		   }
		}

		$this->checkClasses();

		closedir( $hdir );
	}

	/**
	 * Carga todas las clases especificadas desde
	 * los directorios de include actuales.
	 */
	public function loadClasses( array $classes )
	{
		for( $i = 0; $i < count($classes); $i++ )
		{
			if( !$this->loadClass( $classes[$i] ) )
			{
				throw new Exception( "Class not found: ". $classes[$i] );
			}
		}
	}


	/**
	 * Carga la clase especificada y verifica que exista.
	 *
	 * @param $classPath Ruta de acceso y nombre de la clase.
	 */
	public function loadClass( $className )
	{
		$classPath = $this->resolveClassPath( $className );

		if ( $classPath == null ) {
			return( null );
		}

		require_once( $classPath );

		if ( !class_exists( $className ) && !interface_exists( $className ) )
		{
			return( null );
		}

		$this->classNames[ $className ] = null;

		return( true );
	}

	/**
	 * Verifica que las clases cargadas sean válidas.
	 */
	private function checkClasses()
	{
		foreach( $this->classNames as $key => $value )
		{
			if ( !class_exists($key) )
			{
				// Si la clase no existe, la
				// elimina de la lista.
				//
				$this->classNames = $this->array_unset( $this->classNames, $key );
			}
		}
	}

	/**
	 * Vincula esta ClassFactory a una clase específica.
	 *
	 * @param $class Nombre de la clase vinculada.
	 */
	public function bindToClass( $class )
	{
		foreach( $this->classNames as $key => $value )
		{
			if ( !$this->classHasParent($key) )
			{
				// Si la clase no extiende a la especificada,
				// la elimina de la lista.
				//
				$this->classNames = $this->array_unset( $this->classNames, $key );
			}
		}

		$this->parentClass = $class;
	}

	/**
	 * Verifica si una clase extiende a otra.
	 *
	 * @param $class       Clase hija.
	 * @param $parentCheck Superclase que se busca.
	 */
	public function classHasParent( $class, $parentCheck )
	{
		$parents = class_parents( $class );

		foreach( $parents as $item )
		{
			if ( $item == $parentCheck ) {
				return( true );
			}
		}

		return( false );
	}

	/**
	 * Devuelve un array con los nombres de las clases
	 * disponibles. Opcionalmente se puede requerir que
	 * busque sólo las clases en el directorio especificado.
	 *
	 * @param $path Directorio de las clases a devolver.
	 */
	public function listClasses( $path = null )
	{
		$classes = array();

		foreach( $this->classNames as $key => $value )
		{
			if ( ($path != null) && ($this->getClassPath( $value ) != null) ) {
				array_push( $classes, $key );
			}
		}

		return( $classes );
	}

	/**
	 * Determina si la clase especificada existe dentro
	 * de la lista de clases de esta ClassFactory.
	 */
	public function exists( $className )
	{
		foreach( $this->classNames as $key => $value )
		{
			if ( $key == $className )
			{
				return( true );
			}
		}

		return( false );
	}

	/**
	 * Crea una instancia de la clase especificada, si existe.
	 *
	 * @param $className Nombre de la clase a crear.
	 */
	public function newInstance( $className )
	{
		if ( !$this->exists( $className ) )
		{
			return( null );
		}

		return( new $className() );
	}

	/**
	 * Devuelve una única instancia (Singleton) de la clase
	 * especificada. Si la instancia no existe, la crea.
	 *
	 * @param $className Nombre de la clase a crear.
	 */
	public function uniqueInstance( $className )
	{
		$object = $this->classNames[$className];

		if ( ( $object == null ) ||
			 ( get_class( $object ) != $className ) )
		{
			$this->classNames[ $className ] = new $className();
		}

		return( $this->classNames[ $className ] );
	}

	/**
	 * Trata de resolver el la ruta de acceso donde
	 * se encuentra el archivo de la clase especificada.
	 *
	 * @param $className Nombre de la clase.
	 *
	 * @return Devuelve la ruta de acceso a la clase,
	 *         o null en caso de no ser válido.
	 */
	public function getClassPath( $className )
	{
		$className = $this->makeFilename( $className );

		foreach( $this->includePath as $path )
		{
			if ( file_exists( $path . $className ) )
			{
				return( $path );
			}
		}

		return( null );
	}

	/**
	 * Trata de resolver el la ruta de acceso donde
	 * se encuentra el archivo de la clase especificada.
	 *
	 * @param $className Nombre de la clase.
	 *
	 * @return Devuelve la ruta de acceso y el nombre del archivo,
	 *         o null en caso de no ser válido.
	 */
	public function resolveClassPath( $className )
	{
		$path = $this->getClassPath( $className );

		$className = $this->makeFilename( $className );

		return( $path ? $path . $className : null );
	}

	/**
	 * Agrega un path a la lista de include paths. Cuando
	 * se trate de cargar una clase, se buscará en todos
	 * los directorios que estén en el <i>include path</i>.
	 *
	 * @param $path Directorio que se agregará.
	 */
	public function addIncludePath( $path )
	{
		array_push( $this->includePath, $this->normalizePath( $path ) );
	}

	/**
	 * Devuelve el nombre del archivo (sin el path) de
	 * la clase especificada.
	 *
	 * @param $className Nombre de la clase.
	 */
	protected function makeFilename( $className )
	{
		$className .= self::$classExtension;

		return( $className );
	}

	/**
	 * Verifica que una ruta de acceso sea válida,
	 * y le da el formato apropiado para utilizarla
	 * en la concatenación con directorios y archivos.
	 *
	 * @param $path Ruta de acceso a verificar.
	 */
	protected function normalizePath( $path )
	{
		$slash = substr( $path, strlen( $path ) - 1, 1);

		if ( ($slash != "/") && ($slash != "\\" ) )
		{
			$path .= ($slash == "\\") ? "\\" : "/" ;
		}

		return( $path );
	}

	/**
	 * Elimina elementos de un array asociativa.
	 *
	 * @param $array  Array de referencia.
	 * @param $keys   Clave o array de claves que se eliminarán.
	 */
	protected function array_unset( $array, $keys )
	{
		if( !is_array( $keys ) )
		{
			$keys = array( $keys );
		}

		$arrayTmp = array_diff_key( $array, array_flip( $keys ) );

		return( $arrayTmp );
	}
}

?>
